#!/usr/bin/env python3
"""Docstring for the sign_in_code_app.py module.

@author Juntong
@date   2022-10-16

签到二维码客户端

"""
import os
import sys
import time
import random
import socket
import functools
import threading
import subprocess
import tkinter as tk

import yaml

try:
    from . import PROJECT_DIR, CONFIG_PATH
except ImportError:
    PROJECT_DIR = os.path.dirname(__file__)
    CONFIG_PATH = os.path.join(PROJECT_DIR, "config.yaml")

try:
    from .. import config_manager
    from . import sign_in_code_core
except ImportError:
    sys.path.append(os.path.dirname(PROJECT_DIR))
    import config_manager
    from desktop_app_ui import sign_in_code_core


def get_ip():
    """
    获取IP, 不考虑多网卡情况
    Returns
    -------
    str
    """
    return socket.gethostbyname(socket.gethostname())


class SignInCodeServerWindow(tk.Frame):
    server_state_btn: tk.Button
    qr_code_data_start_entry: tk.Entry
    qr_code_refresh_btn: tk.Button
    qr_code_auto_refresh_btn: tk.Button
    open_app_config_btn: tk.Button


class SignInCodeApp(tk.Frame):
    def __init__(self, master, menu_bar: None | tk.Menu = None):
        super().__init__(master)

        # 先设置属性值
        self.app_config = {}
        self.app_config_update_time = None

        if menu_bar is None:
            self.menu_bar = tk.Menu(self)
            master["menu"] = self.menu_bar
        else:
            self.menu_bar = menu_bar
        self.menu_bar_window = tk.Menu(self.menu_bar, tearoff=False)
        self.top_message = tk.Label(self)
        self.image_window = tk.Label(self)
        self.image_window_photo = None
        self.qr_server_threading: None or threading.Thread = None
        self.server_window = SignInCodeServerWindow(self)
        self.backstage_threading: None or threading.Thread = None
        self.app_windows = [self.image_window, self.server_window]
        self.showing_window = None
        self.qr_code_auto_refresh_threading: None or threading.Thread = None

        # 读取配置
        self.update_app_config()
        # 初始化界面组件
        self.setup_menu_bar()
        self.setup_top_message()
        self.top_message.pack(fill="x", side="top", expand=1)  # top message 组件应该被优先挂载
        self.setup_image_window()
        self.setup_server_window()

        self.switch_window_to(0)

        # 启动后台线程
        if self.backstage_threading:
            self.backstage_threading_stop()
        self.backstage_threading = threading.Thread(target=self.backstage_func, daemon=True)
        self.backstage_threading.start()

    def update_app_config(self):
        with open(CONFIG_PATH, "r", encoding="utf-8") as fp:
            config_data = yaml.safe_load(fp)
        for key in config_data.keys():
            if key.endswith("path"):
                config_data[key] = config_manager.path_formate(config_data[key],
                                                               formate_mapping={"PROJECT_PATH": PROJECT_DIR})
        self.app_config.update(config_data)
        self.app_config_update_time = time.time()

    def setup_menu_bar(self):
        """
        初始化顶部菜单栏
        """
        self.menu_bar_window.add_command(label="QR Code", command=functools.partial(self.switch_window_to, 0))
        self.menu_bar_window.add_command(label="QR Server", command=functools.partial(self.switch_window_to, 1))

        self.menu_bar.add_cascade(label="QR App", menu=self.menu_bar_window)

    def setup_top_message(self):
        """
        初始化顶部文本
        """
        self.top_message["text"] = self.app_config.get("sign_in_app_top_message", "Top Message")
        self.top_message["bg"] = self.app_config.get("sign_in_app_top_bg", "yellow")

    def setup_image_window(self):
        """
        初始化图片窗口
        """
        self.image_window_show(self.app_config["sign_in_app_init_photo_path"])

    def setup_server_window(self):
        """
        初始化服务器管理窗口
        """
        self.server_window.server_state_btn = tk.Button(self.server_window, command=self.server_state_reverse)
        self.server_window.server_state_btn.pack(fill="x", expand=1, pady=1)

        self.server_window.qr_code_data_start_entry = tk.Entry(self.server_window)
        self.server_window.qr_code_data_start_entry.insert(0, "http://" + get_ip() + ":5001/SignIn/")
        self.server_window.qr_code_data_start_entry.pack(fill="x", expand=1, pady=1)

        self.server_window.qr_code_refresh_btn = tk.Button(self.server_window, text="手动刷新二维码",
                                                           command=self.qr_code_refresh_command)
        self.server_window.qr_code_refresh_btn.pack(fill="x", expand=1, pady=1)

        self.server_window.qr_code_auto_refresh_btn = tk.Button(self.server_window,
                                                                command=self.qr_code_auto_refresh_thread_state_reverse)
        self.server_window.qr_code_auto_refresh_btn.pack(fill="x", expand=1, pady=1)

        self.server_window.open_app_config_btn = tk.Button(self.server_window, text="打开配置文件",
                                                           command=self.open_app_config_command)
        self.server_window.open_app_config_btn.pack(fill="x", expand=1, pady=2)

        # 手动刷新一次服务器窗口数据
        self.update_server_window()

    def qr_code_refresh_command(self):
        """手动刷新二维码, 调用 self.qr_code_update 并填入 self.gen_random_code 生成的数据"""
        append_data = self.gen_random_code()
        self.qr_code_update(append_data)

    def open_app_config_command(self) -> subprocess.Popen:
        """
        打开应用的配置文件, 打开方式写在配置文件里
        Returns
        -------
        subprocess.Popen
        """
        cmd: str = self.app_config["sign_in_app_open_config_exec"]
        cmd = cmd.format(CONFIG_PATH)
        p = subprocess.Popen(cmd,
                             stdin=sys.stdin,
                             stdout=sys.stdout,
                             stderr=sys.stderr,
                             shell=True)
        return p

    def qr_code_auto_refresh_thread_state(self):
        """
        返回二维码自动刷新线程的是否在运行
        Returns
        -------
        bool
        """
        return self.qr_code_auto_refresh_threading is not None and self.qr_code_auto_refresh_threading.is_alive()

    def qr_code_auto_refresh_thread_start(self):
        """
        启动二维码自动刷新线程
        """
        if not self.qr_code_auto_refresh_thread_state():
            self.qr_code_auto_refresh_threading = threading.Thread(target=self.qr_code_auto_refresh_thread_func,
                                                                   daemon=True)
            self.qr_code_auto_refresh_threading.start()

    def qr_code_auto_refresh_thread_stop(self):
        """
        （试图）停止二维码自动刷新线程
        """
        thr = self.qr_code_auto_refresh_threading
        self.qr_code_auto_refresh_threading = None
        if thr and thr.is_alive():
            thr.join()

    def qr_code_auto_refresh_thread_state_reverse(self):
        """
        反转二维码自动刷新线程的运行状态
        """
        if self.qr_code_auto_refresh_thread_state():
            self.qr_code_auto_refresh_thread_stop()
        else:
            self.qr_code_auto_refresh_thread_start()

    def qr_code_auto_refresh_thread_func(self):
        """
        二维码自动刷新线程执行函数
        """
        last = -1
        while self.qr_code_auto_refresh_threading:
            now = int(time.time())
            if now - last >= self.app_config["sign_in_app_qr_refresh_time"]:
                try:
                    self.qr_code_update(self.gen_random_code())
                except AssertionError as err:
                    print("qr_code_auto_refresh_thread_func error", err)
                else:
                    last = now
            time.sleep(1)

    def qr_code_update_callback(self, data):
        """
        当二维码刷新后执行的回调函数
        Parameters
        ----------
        data: str
            二维码的数据内容
        """

    def qr_code_update(self, data_append: str):
        """
        手动刷新二维码
        Parameters
        ----------
        data_append: str
            附加在后面的数据
        """
        assert self.server_state_get(), "服务器未开启"
        data = self.server_window.qr_code_data_start_entry.get() + data_append
        res = sign_in_code_core.fast_send({"command": "gen", "code data": data})
        assert not res.get("error"), "二维码生成错误"
        # 显示二维码
        qrcode_path = res["path"]
        m, p = self.app_config.get("sign_in_app_qr_showing_parameter", (None, 1))
        try:
            self.image_window_show(qrcode_path, m, p)
        except ValueError as err:
            print(err)
            self.image_window_show(qrcode_path)
        # 执行回调函数
        self.qr_code_update_callback(data)

    def backstage_threading_stop(self):
        """
        (试图)停止后台进程
        """
        thr = self.backstage_threading
        self.backstage_threading = None
        if thr and thr.is_alive():
            thr.join()

    def update_server_window(self):
        """
        刷新服务器管理窗口内容
        """
        if self.server_state_get():
            self.server_window.server_state_btn["text"] = "二维码服务器已启动, 点击关闭"
            self.server_window.server_state_btn["bg"] = "green"
        else:
            self.server_window.server_state_btn["text"] = "二维码服务器未启动, 点击开启"
            self.server_window.server_state_btn["bg"] = "Crimson"

        if self.qr_code_auto_refresh_thread_state():
            self.server_window.qr_code_auto_refresh_btn["text"] = "正在自动刷新二维码, 点击关闭"
            self.server_window.qr_code_auto_refresh_btn["bg"] = "green"
        else:
            self.server_window.qr_code_auto_refresh_btn["text"] = "未在自动刷新二维码, 点击启动"
            self.server_window.qr_code_auto_refresh_btn["bg"] = "Crimson"

        # self.update_server_window_size()

    def update_server_window_size(self):
        """
        更新 server_window 大小
        """
        self.image_window.update()
        self.server_window.update()
        iw_w = self.image_window.winfo_reqwidth()
        sw_w = self.server_window.winfo_reqwidth()
        iw_h = self.image_window.winfo_reqheight()
        sw_h = self.image_window.winfo_reqheight()
        w = max(iw_w, sw_w)
        h = max(iw_h, sw_h)
        self.server_window["width"] = w
        self.server_window["height"] = h

        self.top_message.update()
        self.server_window.server_state_btn["width"] = self.top_message.winfo_reqwidth() - 10

        return w, h

    def backstage_func(self):
        while self.backstage_threading:
            if self.showing_window is self.server_window:
                self.update_server_window()

            if os.path.getmtime(CONFIG_PATH) > self.app_config_update_time:
                self.update_app_config()
            time.sleep(1)

    def server_state_get(self):
        return self.qr_server_threading is not None and self.qr_server_threading.is_alive()

    def server_state_reverse(self):
        if self.server_state_get():
            # 服务开启，点击关闭
            sign_in_code_core.fast_send({"command": "close"})
        else:
            # 服务未启动，点击关闭
            self.qr_server_threading = threading.Thread(target=sign_in_code_core.fast_start_server, daemon=True)
            self.qr_server_threading.start()

    def image_window_show(self, img_path, modify: None | str = None, proportion: int = 1):
        """
        在 image_window 上显示指定的图片
        Parameters
        ----------
        img_path: str
            图片的位置
        modify: None or str
            当 modify 为 None 时, proportion 无效。
            当 modify 非 None, 其值应为 "zoom" 或 "subsample" 中的一个; "zoom" 代表放大, "subsample" 代表缩小
        proportion: int
            放大或缩小的比例。
        """
        pi = tk.PhotoImage(file=img_path)
        if modify is None:
            self.image_window_photo = pi
        elif modify == "zoom":
            self.image_window_photo = pi.zoom(proportion)
        elif modify == "subsample":
            self.image_window_photo = pi.subsample(proportion)
        else:
            raise ValueError(type(self).__name__ +
                             '.image_window_show 的参数`modify`值应为 [None, "zoom", "subsample"]中的一个, got', modify)
        self.image_window["image"] = self.image_window_photo

    def switch_window_to(self, window_index):
        """

        Parameters
        ----------
        window_index: int
            窗口序号
        """
        if self.showing_window:
            self.showing_window.pack_forget()
        try:
            self.showing_window = self.app_windows[window_index]
        except IndexError as err:
            self.showing_window = None
            raise IndexError(self, "method `switch_window_to`", err.args)
        else:
            self.showing_window.pack(fill="both", expand=1)

    @staticmethod
    def gen_random_code():
        ret = []
        for _ in range(3):
            ret.append(random.randint(ord("a"), ord("z")))
        for _ in range(2):
            ret.append(random.randint(ord("A"), ord("Z")))
        for _ in range(3):
            ret.append(random.randint(ord("0"), ord("9")))
        random.shuffle(ret)
        return "".join(chr(i) for i in ret)

    def __del__(self):
        # 关闭签到码服务器
        if self.server_state_get():
            sign_in_code_core.fast_send({"command": "close"})
        # 关闭二维码自动刷新
        self.qr_code_auto_refresh_thread_stop()
        # 关闭后台线程
        self.backstage_threading_stop()

        if hasattr(super(), "__del__"):
            getattr(super(), "__del__")()


if __name__ == "__main__":
    root = tk.Tk()
    win = SignInCodeApp(root)
    win.pack(expand=1)
    root.mainloop()
